#!/usr/bin/env python
from importlib import import_module
import os, gc, threading, time
import json
import cv2
import base64
import uuid
import numpy as np
from flask import Flask, Response, jsonify, request
from flask_cors import CORS
import rospy
from std_msgs.msg import String
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
from flask_mqtt import Mqtt
from flask_ros.msg import Angle, Location, DetecInfo, EventInfo
from flask_ros.srv import ShowPCD, SetDetecEvent, GetConfig, SetLineOrRect, ShowLine, SetDetecParams, SetTopic


app = Flask(__name__)
app.config['MQTT_BROKER_URL'] = '192.168.50.26'  
app.config['MQTT_BROKER_PORT'] = 1883
app.config['MQTT_USERNAME'] = 'test2'
app.config['MQTT_PASSWORD'] = '123456'  
app.config['MQTT_TLS_ENABLED'] = False
app.config['MQTT_CLEAN_SESSION'] = True
app.config['MQTT_REFRESH_TIME'] = 1.0
app.config['MQTT_KEEPALIVE'] = 60

CORS(app, supports_credentials=True)

mqtt = Mqtt(app)

@mqtt.on_connect()
def handle_connect(client, userdata, flags, rc):
    if rc == 0:
        print('Receive client connected successfully')
    else:
        print('Bad connection. Code:', rc)


class Queue:
    def __init__(self, length) -> None:
        self.length = 50
        self.stack = []
    
    def write(self, img) -> None:
        self.stack.append(img)
        if len(self.stack) >= self.length:
            # clear list
            del self.stack[:]
            gc.collect()
    
    def getFrame(self):
        # print('length of stack is ', len(self.stack))
        if len(self.stack) <= 0:
            return None
        else:
            value = self.stack.pop()
            if len(self.stack) >= self.length:
                del self.stack[:]
                gc.collect()
            return value


class SQLite:
    def __init__(self) -> None:
        self.db = None
        self.base_camera = {
            "cameraAddress": "江西九江",
            "cameraID": "hik_01",
            "cameraName": "hik",
            "cameraTopic": "/hik_cam_node/hik_camera",
            "createTime": time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())),
            "id": uuid.uuid4().hex,
            "isUsing": True,
            "relevanceRSU": "rsu_01"
        }
        self.base_lidar = {
            "createTime": time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())),
            "id": uuid.uuid4().hex,
            "isUsing": True,
            "lidarAddress": "江西九江",
            "lidarID": "livox_01",
            "lidarName": "livox",
            "lidarTopic": "/livox/lidar",
            "relevanceRSU": "rus_01"
        }

    def add(self, data):
        if data.get('type') == 'camera':
            self.db['camera'].append({
                'id': uuid.uuid4().hex,
                'cameraName': data.get('msg')['cameraName'],
                'cameraID': data.get('msg')['cameraID'],
                'cameraTopic': data.get('msg')['cameraTopic'],
                'cameraAddress': data.get('msg')['cameraAddress'],
                'relevanceRSU': data.get('msg')['relevanceRSU'],
                'isUsing': False,
                'createTime': time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
            })
            return True
        if data.get('type') == 'lidar':
            self.db['lidar'].append({
                'id': uuid.uuid4().hex,
                'lidarName': data.get('msg')['lidarName'],
                'lidarID': data.get('msg')['lidarID'],
                'lidarTopic': data.get('msg')['lidarTopic'],
                'lidarAddress': data.get('msg')['lidarAddress'],
                'relevanceRSU': data.get('msg')['relevanceRSU'],
                'isUsing': False,
                'createTime': time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
            })
            return True
        return False
    

    def delete(self, data):
        if data.get('type') == 'camera':
            id = data.get('id')
            for cam in self.db['camera']:
                if cam['id'] == id:
                    self.db['camera'].remove(cam)
                    if len(self.db['camera']) == 0:
                        self.add(self.base_camera)
                    return True
        if data.get('type') == 'lidar':
            id = data.get('id')
            for lidar in self.db['lidar']:
                if lidar['id'] == id:
                    self.db['lidar'].remove(lidar)
                    if len(self.db['lidar']) == 0:
                        self.add(self.base_lidar)
                    return True
        return False
    

    def change(self, data):
        if data.get('type') == 'camera':
            msg = data.get('msg')
            for cam in self.db['camera']:
                if cam['id'] == msg['id']:
                    cam['cameraName'] = msg['cameraName']
                    cam['cameraID'] = msg['cameraID']
                    cam['cameraTopic'] = msg['cameraTopic']
                    cam['cameraAddress'] = msg['cameraAddress']
                    cam['relevanceRSU'] = msg['relevanceRSU']
                    # cam['isUsing'] = msg['isUsing']
                    cam['createTime'] = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
                    return True
        if data.get('type') == 'lidar':
            msg = data.get('msg')
            for lidar in self.db['lidar']:
                if lidar['id'] == msg['id']:
                    lidar['lidarName'] = msg['lidarName']
                    lidar['lidarID'] = msg['lidarID']
                    lidar['lidarTopic'] = msg['lidarTopic']
                    lidar['lidarAddress'] = msg['lidarAddress']
                    lidar['relevanceRSU'] = msg['relevanceRSU']
                    # lidar['isUsing'] = msg['isUsing']
                    lidar['createTime'] = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
                    return True
        return False
    
    
    def find(self, type, id):
        if type is None:
            return self.db
        if type == 'camera':
            if id is None:
                return self.db['camera']
            else:
                for msg in self.db['camera']:
                    if id == msg['id']:
                        return [msg]
                return []
        if type == 'lidar':
            if id is None:
                return self.db['lidar']
            else:
                for msg in self.db['lidar']:
                    if id == msg['id']:
                        return [msg]
                return []
        

class MyJson:
    def __init__(self, path) -> None:
        self.path = path
    
    def write(self, data) -> None:
        with open(self.path, "w+", encoding='utf-8_sig') as f:
            json.dump(data, f , sort_keys=True, indent =4, ensure_ascii=False)

    def read(self):
        with open(self.path, "r+", encoding='utf-8_sig') as f:
            data=json.load(f)
        return data

db_path = "./src/ros_flask/data/db.json"    

fakeSQL = SQLite()
myJson = MyJson(path=db_path)
# load the db when software is running
fakeSQL.db = myJson.read()


CAMERAS = [
    {
        "id": "001",
        "cameraName": "hik",
        "cameraID": "hik_01",
        "cameraTopic": "rtsp://admin:js123456@192.168.50.70:554/Streaming/Channels/102",
        "cameraAddress": "jiujiang",
        "relevanceRSU": "rus_01",
        "createTime": time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
    }
]


def cv2_base64(image):
  base64_str = cv2.imencode('.jpg',image)[1].tobytes()
  base64_str = base64.b64encode(base64_str)
  return base64_str


def base64_cv2(base64_str):
  imgString = base64.b64decode(base64_str)
  nparr = np.frombuffer(imgString,np.uint8)
  image = cv2.imdecode(nparr,cv2.IMREAD_COLOR)
  return image


def img_callback(data):
    # print('length of stack is ', len(queue.stack))
    bridge = CvBridge()
    img = bridge.imgmsg_to_cv2(data, 'bgr8')
    # cv2.imshow("Demo", img)
    # cv2.waitKey(50)
    # add to queue
    queue.write(img=img)


def detecInfo_callback(data):
    seq_location = []
    for seq_lo in data.seq_location:
        location = []
        for lo in seq_lo.location:
            point = {
                'x': lo.x,
                'y': lo.y,
                'z': lo.z
            }
            location.append(point)
        seq_location.append(location)
    
    seq_angle = []
    for seq_ang in data.seq_angle:
        vec_angle = []
        for ang in seq_ang.angle:
            vec_angle.append(ang)
        seq_angle.append(vec_angle)

    msg = {
        'id': data.id,
        'confid': data.confid,
        'speed': data.speeds,
        'dist': data.dist,
        'location': seq_location,
        'angle': seq_angle
    }
    publish_result = mqtt.publish(topic='detec_info', payload=json.dumps(msg))


def eventInfo_callback(data):
    bridge = CvBridge()
    img = bridge.imgmsg_to_cv2(data.img, 'bgr8')
    msg = {
        'type': data.type,
        'time': data.time,
        'event_name': data.event_name,
        'level': data.level,
        'judge': data.judge
    }
    mqtt.publish(topic='event_info', payload=json.dumps(msg))
    mqtt.publish(topic='event_img', payload=cv2_base64(img))


def runRos():
    rospy.init_node('flask_node', disable_signals=True)
    rospy.Subscriber('/hik_img', Image, img_callback)
    rospy.Subscriber('/detecInfo', DetecInfo, detecInfo_callback)
    rospy.Subscriber('/eventInfo', EventInfo, eventInfo_callback)
    rospy.spin()


def generation():
    while True:
        time.sleep(0.15)
        frame = queue.getFrame()
        if frame is not None:
            # print(type(frame))
            detec_frame = cv2.imencode('.jpg', frame)[1].tobytes()
            yield (b'--frame\r\n' 
                   b'Content-Type: image/jpeg\r\n\r\n' + detec_frame + b'\r\n--frame\r\n')


execution_path = os.getcwd()
queue = Queue(length=50)
thread_ros = threading.Thread(target=runRos)
thread_ros.start()


@app.route('/publish', methods=['POST'])
def publish_message():
   request_data = request.get_json()
   print(request_data['topic'])
   print(request_data['msg'])
   publish_result = mqtt.publish(topic=request_data['topic'], payload=request_data['msg'])
   return jsonify({'code': publish_result[0]})


@app.route('/getConfig', methods=['GET'])
def getConfig():
    state = 1
    error = None
    try:
        get_config_client = rospy.ServiceProxy('get_config', GetConfig)
        res = get_config_client.call()
        config = {
            'showPCD': res.showPCD,
            'eventState': res.eventState,
            'object_detec': res.objectDetec,
            'event_detec': res.eventDetec
        }
    except rospy.ServiceException as e:
        print('call service failed: ', e)
        error = e
        state = 0
    if state:
        return jsonify({'code': state, 'config': config})
    else:
        return jsonify({'code': state, 'error': error})
    


@app.route('/set', methods=['GET', 'POST'])
def setFunc():
    if request.method == "GET":
        type = request.args.get('type')
        status = 1      # status
        error = None
        if type == 'pcd':
            flag = request.args.get('flag')
            # rospy.wait_for_service('show_pcd')
            show_pcd_client = rospy.ServiceProxy('show_pcd', ShowPCD)
            try:
                res = show_pcd_client.call(flag)
            except rospy.ServiceException as e:
                print('call service failed: ', e)
                status = 0
                error = e
            return jsonify({'status': status, 'error': error})
        elif type == 'line':
            flag = request.args.get('flag')
            show_line_client = rospy.ServiceProxy('show_line', ShowLine)
            try:
                res = show_line_client.call(flag)
            except rospy.ServiceException as e:
                print('call service failed: ', e)
                status = 0
                error = e
            return jsonify({'status': status, 'error': error})

        elif type == 'event':
            event_index = request.args.get('event_index')
            flag = request.args.get('flag')
            try:
                set_event_client = rospy.ServiceProxy('set_event', SetDetecEvent)
                res = set_event_client.call(int(event_index), flag)
            except rospy.ServiceException as e:
                print ("Service call failed: %s", e)
                status = 0
                error = e
            return jsonify({'status': status, 'error': error})
            

    if request.method == "POST":
        post_state = 1
        post_error = None
        json_data = None
        if request.content_type.startswith('application/json'):
            # print('application/json')
            json_data = request.get_json()
        else:
            return jsonify({'error': 'request formate must be application/json'})

        if 'eventIndex' in json_data:
            eventIndex = json_data['eventIndex']
            data = json_data['data']
            data_list = []
            if eventIndex > 1:     # ROI
                """[{'x': 100, 'y': 100, 'w': 200, 'h': 200}]"""
                data_list.append(data[0]['x'])
                data_list.append(data[0]['y'])
                data_list.append(data[0]['w'])
                data_list.append(data[0]['h'])
            else:                   # point
                """[{'x': 541, 'y': 229}, {'x': 533, 'y': 254}]"""
                data_list.append(data[0]['x'])
                data_list.append(data[0]['y'])
                data_list.append(data[1]['x'])
                data_list.append(data[1]['y'])
            # print(eventIndex)
            print(data_list)
            try:
                set_roi = rospy.ServiceProxy('set_line_roi', SetLineOrRect)
                res = set_roi.call(eventIndex, data_list)
            except rospy.ServiceException as e:
                print ("Service call failed: %s", e)
                post_state = 0
                post_error = e
            return jsonify({'state': post_state, 'error': post_error})
        else:
            detec_type = json_data['type']
            detec_val = json_data['val']
            if detec_type == 'object_detec' and detec_val < 2:
                return jsonify({'state': 1, 'error': 'val must large than 2'})
            if detec_type == 'event_detec' and detec_val < 1:
                return jsonify({'state': 1, 'error': 'val must large than 1'})
            
            print('type {} | val: {}'.format(detec_type, detec_val))
            try:
                set_detec_params = rospy.ServiceProxy('set_detec_params', SetDetecParams)
                res = set_detec_params.call(detec_type, int(detec_val))
            except rospy.ServiceException as e:
                print ("Service call failed: %s", e)
                post_state = 0
                post_error = e
            return jsonify({'state': post_state, 'error': post_error})
            


@app.route('/video_feed')
def video_feed():
    print("execution_path: ", execution_path)
    return Response(generation(), mimetype='multipart/x-mixed-replace; boundary=frame')

#######################################################################################

def isHasTopic(topic):
    topics = rospy.get_published_topics()
    for t_tuple in topics:
        if t_tuple[0] == topic:
            return True
    return False


def updateInUsingState(type, id):
    if type == 'camera':
        for msg in fakeSQL.db['camera']:
            if msg['id'] == id:
                msg['isUsing'] = True
            else:
                msg['isUsing'] = False
    else:
        for msg in fakeSQL.db['lidar']:
            if msg['id'] == id:
                msg['isUsing'] = True
            else:
                msg['isUsing'] = False
    myJson.write(fakeSQL.db)


@app.route('/setTopic', methods=['GET', 'POST'])
def test():
    if request.method == "POST":
        json_data = None
        if request.content_type.startswith('application/json'):
            json_data = request.get_json()
        else:
            return jsonify({'code': 0, 'error': 'request formate must be application/json'})
        
        device_type = json_data['type']
        msg_id = json_data['id']
        flag = json_data['flag']
        msg = fakeSQL.find(type=device_type, id=msg_id)

        if len(msg) == 0:
            return jsonify({"code": 0, "error": "id: {} is not exist".format(msg_id)})
        
        if flag:
            topic_val = None
            if device_type == 'camera':
                topic_val = msg[0]['cameraTopic']
            else:
                topic_val = msg[0]['lidarTopic']
            # print(isHasTopic(topic_val))
            print("topic_val ", topic_val)
            if isHasTopic(topic_val):
                try:
                    set_topic_client = rospy.ServiceProxy('set_topic', SetTopic)
                    set_topic_client.call(device_type, topic_val)
                    # update isUsing state
                    updateInUsingState(type=device_type, id=msg_id)
                    return jsonify({'code': 1, 'error': None})
                except rospy.ServiceException as e:
                    print ("Service call failed: %s", e)
                    return jsonify({'code': 0, 'error': e})
            else:
                error = "topic {} is not find in current ros system".format(topic_val)
                return jsonify({"code": 0, "error": error})
        else:
            tmp_topic = "/kbw"
            try:
                set_topic_client = rospy.ServiceProxy('set_topic', SetTopic)
                set_topic_client.call(device_type, tmp_topic)
                for msg in fakeSQL.db[device_type]:
                    if msg['id'] == msg_id:
                        msg['isUsing'] = False
                myJson.write(fakeSQL.db)
                return jsonify({'code': 1, 'error': None})
            except rospy.ServiceException as e:
                print ("Service call failed: %s", e)
                return jsonify({'code': 0, 'error': e})
    return jsonify({"code": 0, "error": "request method must be POST" })



@app.route('/add', methods=['POST'])
def add():
    if request.method == 'POST':
        data = None
        if request.content_type.startswith('application/json'):
            data = request.get_json()
            print(data)
           
        if fakeSQL.add(data):
            # update db
            myJson.write(fakeSQL.db)
            return jsonify({"code": 1})
    return jsonify({"code": 0})
    


@app.route('/delete', methods=['POST'])
def delete():
    if request.method == 'POST':
        if request.content_type.startswith('application/json'):
            data = request.get_json()
            if fakeSQL.delete(data):
                myJson.write(fakeSQL.db)
                return jsonify({"code": 1})
            else:
                return jsonify({"code": 0})


@app.route('/change', methods=['POST'])
def change():
    if request.method == 'POST':
        if request.content_type.startswith('application/json'):
            data = request.get_json()
            if fakeSQL.change(data):
                myJson.write(fakeSQL.db)
                return jsonify({"code": 1})
            else:
                return jsonify({"code": 0})


@app.route('/find', methods=['GET'])
def find():
    if request.method == 'GET':
        type = request.args.get('type')
        id = request.args.get('id')
        res = fakeSQL.find(type=type, id=id)
        return jsonify({'code': 1, "msg": res})
    


if __name__ == '__main__':
    app.run(host='0.0.0.0', threaded=True, use_reloader=False, debug=False)
    # server = pywsgi.WSGIServer((), app)
